Modal.tsx ➔ Modal   F
last analyzed

Complexity

Conditions 17

Size

Total Lines 144
Code Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 107
dl 0
loc 144
rs 1.2599
c 0
b 0
f 0
cc 17

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Modal.tsx ➔ Modal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import React, { createContext, useContext, useEffect, useRef } from "react";
2
import { createPortal } from "react-dom";
3
import { getFocusableElements } from "../helpers/focus";
4
5
interface ModalProps {
6
  id: string;
7
  parentElement: Element | null;
8
  visible: boolean;
9
  children: React.ReactNode;
10
  className?: string;
11
  onModalConfirm: (e: React.MouseEvent<HTMLButtonElement>) => void;
12
  onModalMiddle?: (e: React.MouseEvent<HTMLButtonElement>) => void;
13
  onModalCancel: (
14
    e: React.MouseEvent<HTMLButtonElement> | KeyboardEvent,
15
  ) => void;
16
}
17
18
// Partial helper allows empty defaults in the createContext call:
19
// https://fettblog.eu/typescript-react/context/#context-without-default-values
20
const modalContext = createContext<Partial<ModalProps>>({});
21
22
export default function Modal({
23
  id,
24
  parentElement,
25
  visible,
26
  children,
27
  className,
28
  onModalConfirm,
29
  onModalMiddle,
30
  onModalCancel,
31
}: ModalProps): React.ReactPortal | null {
32
  // Set up div ref to measure modal height
33
  const modalRef = useRef<HTMLDivElement>(null);
34
35
  const getFocusableModalElements = () =>
36
    modalRef && modalRef.current ? getFocusableElements(modalRef.current) : [];
37
38
  const handleTabKey = (e: KeyboardEvent): void => {
39
    if (modalRef && modalRef.current) {
40
      const focusableModalElements = getFocusableModalElements();
41
42
      if (focusableModalElements.length === 0) {
43
        e.preventDefault(); // TODO: should this throw an error?
44
        return;
45
      }
46
47
      const firstElement = focusableModalElements[0] as HTMLElement;
48
      const lastElement = focusableModalElements[
49
        focusableModalElements.length - 1
50
      ] as HTMLElement;
51
52
      if (focusableModalElements.length === 1) {
53
        // This check to avoid strange behaviour if firstElement == lastElement.
54
        firstElement.focus();
55
        e.preventDefault();
56
        return;
57
      }
58
59
      const focusableModalElementsArray = Array.from(focusableModalElements);
60
61
      if (
62
        document.activeElement &&
63
        !focusableModalElementsArray.includes(
64
          document.activeElement as HTMLElement,
65
        )
66
      ) {
67
        firstElement.focus();
68
        e.preventDefault();
69
        return;
70
      }
71
72
      if (!e.shiftKey && document.activeElement === lastElement) {
73
        firstElement.focus();
74
        e.preventDefault();
75
        return;
76
      }
77
78
      if (e.shiftKey && document.activeElement === firstElement) {
79
        lastElement.focus();
80
        e.preventDefault();
81
      }
82
    }
83
  };
84
85
  // Collection of key codes and event listeners
86
  const keyListenersMap = new Map([
87
    [27, onModalCancel],
88
    [9, handleTabKey],
89
  ]);
90
91
  // Runs every time visible changes to set the overflow on the modal and update the body overflow
92
  useEffect((): (() => void) => {
93
    function setBodyStyle(): void {
94
      document.body.style.overflow = visible ? "hidden" : "visible";
95
    }
96
    setBodyStyle();
97
    // Runs on component unmount
98
    return (): void => {
99
      setBodyStyle();
100
    };
101
  }, [visible]);
102
103
  // Adds various key commands to the modal
104
  useEffect((): (() => void) => {
105
    let keyListener;
106
    if (visible) {
107
      keyListener = (e: KeyboardEvent): void => {
108
        const listener = keyListenersMap.get(e.keyCode);
109
        return listener && listener(e);
110
      };
111
      document.addEventListener("keydown", keyListener);
112
    }
113
114
    return (): void => {
115
      if (keyListener !== undefined) {
116
        document.removeEventListener("keydown", keyListener);
117
      }
118
    };
119
  }, [keyListenersMap, visible]);
120
121
  // Focus the first focusable element when the modal becomes visible.
122
  useEffect(() => {
123
    if (visible) {
124
      const focusableModalElements = getFocusableModalElements();
125
      if (focusableModalElements.length > 0) {
126
        const firstElement = focusableModalElements[0] as HTMLElement;
127
        firstElement.focus();
128
      }
129
    }
130
  }, [visible]);
131
132
  if (parentElement !== null) {
133
    return createPortal(
134
      <div
135
        aria-describedby={`${id}-description`}
136
        aria-hidden={!visible}
137
        aria-labelledby={`${id}-title`}
138
        data-c-dialog={visible ? "active--overflowing" : ""}
139
        data-c-padding="top(double) bottom(double)"
140
        role="dialog"
141
        ref={modalRef}
142
        className={className}
143
        data-c-visibility={!visible ? "hidden" : ""}
144
      >
145
        <div data-c-background="white(100)" data-c-radius="rounded">
146
          <modalContext.Provider
147
            value={{
148
              id,
149
              parentElement,
150
              visible,
151
              onModalConfirm,
152
              onModalMiddle,
153
              onModalCancel,
154
            }}
155
          >
156
            {children}
157
          </modalContext.Provider>
158
        </div>
159
      </div>,
160
      parentElement,
161
    );
162
  }
163
164
  return null;
165
}
166
167
Modal.Header = function ModalHeader({ children }): React.ReactElement {
168
  return <div className="dialog-header">{children}</div>;
169
};
170
171
Modal.Body = function ModalBody(props): React.ReactElement {
172
  const { children } = props;
173
  return <div data-c-border="bottom(thin, solid, black)">{children}</div>;
174
};
175
176
Modal.Footer = function ModalFooter(props): React.ReactElement {
177
  const { children } = props;
178
  return (
179
    <div data-c-padding="normal">
180
      <div data-c-grid="gutter middle">
181
        {Array.isArray(children) && children.length > 0
182
          ? children.map(
183
              (btn, index): React.ReactElement => (
184
                <div
185
                  // eslint-disable-next-line react/no-array-index-key
186
                  key={index}
187
                  data-c-grid-item={`base(1of${children.length})`}
188
                >
189
                  {btn}
190
                </div>
191
              ),
192
            )
193
          : children}
194
      </div>
195
    </div>
196
  );
197
};
198
199
Modal.FooterConfirmBtn = function ConfirmBtn(props): React.ReactElement {
200
  const { onModalConfirm } = useContext(modalContext);
201
  return (
202
    <div data-c-alignment="base(right)">
203
      <button
204
        {...props}
205
        data-c-button="solid(c1)"
206
        data-c-dialog-action="close"
207
        data-c-radius="rounded"
208
        type="button"
209
        onClick={onModalConfirm}
210
      />
211
    </div>
212
  );
213
};
214
215
Modal.FooterCancelBtn = function CancelBtn(props): React.ReactElement {
216
  const { onModalCancel } = useContext(modalContext);
217
  return (
218
    <div>
219
      <button
220
        {...props}
221
        data-c-button="outline(c1)"
222
        data-c-dialog-action="close"
223
        data-c-radius="rounded"
224
        type="button"
225
        onClick={onModalCancel}
226
      />
227
    </div>
228
  );
229
};
230
231
Modal.FooterMiddleBtn = function MiddleBtn(props): React.ReactElement {
232
  const { onModalMiddle } = useContext(modalContext);
233
  return (
234
    <div data-c-alignment="base(center)">
235
      <button
236
        {...props}
237
        data-c-button="solid(c1)"
238
        data-c-dialog-action="close"
239
        data-c-radius="rounded"
240
        type="button"
241
        disabled={onModalMiddle === undefined}
242
        onClick={onModalMiddle}
243
      />
244
    </div>
245
  );
246
};
247